Vue3学习
参考资料
首先当然是 官方的 Vue3.0 文档 参考资料 Vue3 中文文档 参考资料 彻底理解服务端渲染 - SSR原理 参考资料 快速使用Vue3最新的15个常用API (这个总结的很棒!)
使用 Vite 构建项目
参考资料 Vite 官方库
# 安装 Vite
npm install -g create-vite-app
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
接下来直接访问 3000 端口
Diff 算法和静态提升
Difference 算法不同,在 Vue2 时是全量比较(一些不会改变的静态 DOM 也需要比较),而到了 Vue3 是给变化的 DOM 打上一个 patch flag
来标识变化,这样那些静态的 DOM 就无需比较了。
hoistStatic 静态提升
- Vue2 中无论元素是否参与更新,每次都会重新创建,然后再渲染
- Vue3 中对于不参与更新的元素,都会静态提升,只会被创建一次,在渲染时直接复用
例如下面转换同一个模板,检查输出内容
<div>
<div>hello vue</div>
<div>hello vue</div>
<div>hello vue</div>
<div>{{ msg }}</div>
</div>
Vue2 的 模板转换工具
// Vue2.x
function render() {
with(this) {
return _c('div', [_c('div', [_v("hello vue")]), _c('div', [_v("hello vue")]),_c('div', [_v("hello vue")]), _c('div', [_v(_s(msg))])
])
}
}
Vue3 的 模板转换工具
// Vue3.x
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
// 可以看到这部分不变的静态 DOM 被抽离了出来
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "hello vue", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "hello vue", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "hello vue", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
// Check the console for the AST
可以看到 Vue3 在变化的部分绑定数据且加上了一个为 1 的 patch flag
标识其为 TEXT,且把不变的部分抽离出来并标识其为 -1(静态提升)
事件侦听缓存
CacheHandlers 事件侦听器缓存,默认情况下 onClick 会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
<div>
<button @click="onClick">按钮</button>
</div>
Vue3 的 模板转换工具
设置 CacheHandlers 之前,可以看到 Vue3.x 还是给其添加了个 8 的静态标记,标明其需要进行追踪
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "按钮", 8 /* PROPS */, ["onClick"])
]))
}
// Check the console for the AST
设置 CacheHandlers 之后,没有这个静态标记了
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "按钮")
]))
}
// Check the console for the AST
Vue2 与 Vue3 代码组织区别
参考资料 简明扼要聊聊 Vue3.0 的 Composition API 是啥东东!
在 Vue2 时的代码组织方式( Options API):
export default {
props: {
……
},
data() {
return {
……
};
},
watch: {
……
},
computed: {
……
}
methods: {
……
}
}
当一个组件里面内容多的化这种代码组织方式就会变的十分臃肿,所以在 Vue3 中引入了 Composition API 这种组织方式
// 例如编写了一个鼠标监听模块 listenMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
function useMouse() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
export default useMouse;
而在组件里就可以直接引用上面的模块使用
<template>
<p>Mouse Position:</p>
<p>x:{{ x }},y:{{ y }}</p>
</template>
<script>
import useMouse from './listenMouse';
export default {
setup (props) {
let {x, y} = useMouse();
return {
x,
y
}
}
}
</script>
通过对 Composition API 的了解,你会发现在代码组织上有一个很大的变化:干掉了 Vue2.x 中神奇的 this
。
在 Vue2.x 中,我们的代码中大量的使用到了 this
,组件中的 props, data,methods 都是绑定到 this
上下文,然后由 this
去访问。
那么使用 Composition API 之后,当涉及到跨组件之间提取、复用逻辑时,就会非常的灵活。一个合成函数只依赖于它的参数和全局引入的 Vue APIs,而不是充满魔法的 this
上下文。我们只需要将组件中你想复用的那部分代码抽离,然后将它导出为函数就可以了。
比如上面中的 listenMouse.js
,单独导出 useMouse,在其他组件都可以使用。
但是这也引出了函数式编程的问题:面向过程通过划分功能模块,通过函数相互间的调用来实现,但是需求变化时就需要更改函数。而你改动的函数有多少的地方在调用它,关联多少数据,这是很不容易弄清楚的地方
回过头来看 Options API 的约定:
- 在
props
里面设置接收参数 - 在
data
里面设置变量 - 在
computed
里面设置计算属性 - 在
watch
里面设置监听属性 - 在
methods
里面设置事件方法
Options APi 约定了我们该在哪个位置做什么事,强制对当前代码进行分割,这样虽然不太灵活但是至少结构上是挺直观的。
Composition API
参考资料 官方文档 什么是组合式 API? 参考资料 VUE 3.0 学习探索入门系列 - Vue3.x 生命周期 和 Composition API 核心语法理解(6)
Vue3 取消了 this
取而代之的是 setup
增加了 2 个参数:
- props,组件参数
- context,上下文信息
setup(props, context) {
// props
// context.attrs
// context.slots
// context.emit
}
基本使用方法如下
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
setup(props) {
console.log(props) // { user: '' }
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
// 组件的“其余部分”
}
setup
是 Composition API 的入口函数,可以说也是整个 Vue3.x 的核心;setup
返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
监听变量的变化:需要使用 ref
来创建变量
注意:ref 只能监听简单类型的数据变化,不能监听复杂类型的变化(对象、数组),复杂类型的变量需要使用 reactive
下面使用一个添加 TODO 的例子来说明使用这个组织方式的好处
模块化:添加 TODO 的例子
创建一个 useRemoveTodo.js
模块专门用于处理移除 TODO 的操作
import { reactive } from 'vue'
// 别忘记了这里也依赖了 vue 的 reactive 模块
// 抽离出来
function useRemoveTodo() {
// 要监听复杂类型需要使用 reactive
const object = reactive({
todies: [
{ id: 1, value: '7 点起床' },
{ id: 2, value: '8 点上课' },
{ id: 3, value: '11 点吃饭' },
{ id: 4, value: '12 点睡觉' }
]
})
function remove(index) {
object.todies = object.todies.filter((value, idx) => idx !== index)
}
return {
object,
remove
}
}
export default useRemoveTodo;
创建一个 useAddTodo.js
模块专门用于添加 TODO 的操作
import { reactive } from 'vue'
// 别忘记了这里也依赖了 vue 的 reactive 模块
// 注意因为修改了 object 这个对象里的 todies 所以需要把 object 传入进来
function useAddTodo(object) {
// 存储用于添加的 Todo
const todo = reactive({
id: 0,
value: ''
})
// 添加 Todo
function addTodo(e) {
// 取消事件的默认动作(这里的提交刷新)
e.preventDefault()
console.log(todo) // Proxy {id: "5", value: "14 点上课"}
// 这个就不解释了,看 JavaScript学习02 那一篇的 Object 对象拷贝
const temp = Object.assign({}, todo)
object.todies.push(temp)
}
return {
todo,
addTodo
}
}
export default useAddTodo
然后就可以直接在组件处引入这两个定义好的模块,使文件变得简洁
<template>
<div>{{ count }}</div>
<button @click="add()">点击 count +1</button>
<br />
<div>
<ul v-for="(item, index) in object.todies" :key="index">
<li @click="remove(index)">{{ item.id }} ==== {{ item.value }}</li>
</ul>
</div>
<br />
<form>
<input type="number" v-model="todo.id" />
<input type="text" v-model="todo.value" />
<input type="submit" @click="addTodo" />
</form>
</template>
<script>
import { ref, reactive } from 'vue'
// 引入自定义的模块
import useRemoveTodo from './module/useRemoveTodo.js'
import useAddTodo from './module/useAddTodo.js'
export default {
setup() {
// 需要监听一个变量变化需要使用 ref 创建
// 这里创建了一个叫做 count 的变量,且其初始值为 0
// 注意:ref 只能监听简单类型的数据变化,不能监听复杂类型的变化(对象、数组)
const count = ref(0)
function add() {
// 注意不能直接用 count++
count.value++
}
// 把各个模块抽离到其它文件去可以导致组件这块保持整洁
const { object, remove } = useRemoveTodo()
const { todo, addTodo } = useAddTodo(object)
// 在组合 API 中定义的变量/方法,想要在外界使用需要使用 return 暴露出去
return {
remove,
count,
object,
add,
addTodo,
todo
}
}
}
</script>
<style scoped>
li {
background-color: blue;
}
</style>
组件的脚本部分的不同写法
参考文档 Vue 装饰器写法 写法参考 Vue Class Component
包装成类的写法
这种包装成类的写法 Vue3 之前就有了,且官方已经在逐步放弃了(详情看:[Abandoned] Class API proposal)
可以看到这种写法可以不写 setup 直接写方法就能调用
<template>
<div>
<button v-on:click="decrement">-</button>
{{ count }}
<button v-on:click="increment">+</button>
</div>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'
// Define the component in class-style
@Component
export default class Counter extends Vue {
// Class properties will be component data
count = 0
// Methods will be component methods
increment() {
this.count++
}
decrement() {
this.count--
}
}
</script>
使用 defineComponent
参考资料 defineComponent
Vue3 TS 的原生写法,我觉得还是转成这种写法好点
import { defineComponent, reactive } from 'vue';
import Parent from '@/components/Parent.vue';
export default defineComponent({
components: {Parent},
setup(){
return reactive({
message: "Hello, Vue3.0 with Typescript"
})
}
})
这个 defineComponent 在 TypeScript 下,给予了组件正确的参数类型推断(就是方便直接 import 其它的 vue 文件)
响应式工具
reactive 包装对象
参考资料 响应性基础 API
reactive
是 Vue3 中提供的实现响应式数据的工具,它能把对象包装成 Proxy 对象,使之用于响应式的特性
- 在 Vue2 中的响应式数据是通过
defineProperty
来实现的 - 而在 Vue3 中响应式数据则是通过 ES6 的 Proxy 来实现的
reactive
参数必须是 对象,如果是普通的参数reactive
无法将其包装成 Proxy 对象,响应式特性就将消失
interface LoginState {
username: string;
password: string;
code: string;
remember: boolean;
}
const state = reactive({
username: "",
password: "",
code: "",
remember: false
}) as LoginState;
ref 包装基本类型数据
ref
底层本质还是一个 reactive
,系统会自动根据我们给 ref
传入的值将它转换成 ref(xx)
--> reactive({ value: xx})
所以在 JS 里取得 ref
的值必须通过 value
获取(在模板中使用不用加 value
)
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
如果使用 TS 需要类型声明:
// 这个 ref 方法返回的类型是一个 Ref 类型
function ref<T>(value: T): Ref<T>
// 这个 Ref 类型的内部
interface Ref<T> {
value: T
}
// 使用例子
const foo = ref<string | number>('foo') // foo's type: Ref<string | number>
foo.value = 123 // ok!
如果泛型的类型未知,则应该使用 ref 转换为 Ref<T>
function useState<State extends string>(initial: State) {
const state = ref(initial) as Ref<State> // state.value -> State extends string
return state
}
如果想要判断参数为 ref,则返回内部值,否则返回参数本身
function useFoo(x: number | Ref<number>) {
// 相当于 val = isRef(val) ? val.value : val
const unwrapped = unref(x) // unwrapped 确保现在是数字类型
}
isRef 的使用,isRef()
函数主要用来判断某个值是否为 ref()
创建出来的对象;
ref 获取标签元素
在 Vue2 中,获取元素都是通过给元素一个 ref 属性,然后通过 this.$refs.xx
来访问的,但这在 Vue3 中已经不再适用了
Vue3 使用 ref
方法创建一个引用
<template>
<div>
<div ref="el">div元素</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// 创建一个DOM引用,名称必须与元素的ref属性名相同
const el = ref(null)
// 在挂载后才能通过 el 获取到目标元素
onMounted(() => {
el.value.innerHTML = '内容被修改'
})
// 把创建的引用 return 出去
return {el}
}
}
</script>
这里举个 Element-Plus 的例子
看文档 Element-plus 还是大量的使用这种 this.$refs
的写法写法
this.$refs[formName].resetFields();
但是在 Vue3 已经不支持这种写法了,所以得了解下这个 $refs
是什么?
<div id="app">
<input type="text" ref="input1"/>
<button @click="add">添加</button>
</div>
new Vue({
el: "#app",
methods:{
add:function(){
this.$refs.input1.value ="22"; //this.$refs.input1 减少获取dom节点的消耗
}
}
})
参考资料 Vue.js の Composition API における this.$refs の取得方法 如上,单纯就是用来取得这个 DOM 或者一个封装的节点,所以只需取得 Element 相应的组件就行了
import { ElForm } from "element-plus";
// ...
// InstanceType 的作用是获取构造函数类型的实例类型
// typeof 可以用于从一个变量上获取它的类型。
// type Instance = InstanceType<typeof TestClass>; // TestClass 固定用法
const formRef = ref<InstanceType<typeof ElForm>>();
// ...
formRef.value?.validate(async valid => {
if (!valid) {
return message.error("请把信息填写完整!");
}
// ...
});
// 最后要暴露这个 ref
return { formRef };
然后再在这个组件上使用 ref 引用
<el-form label-width="70px" :rules="rules" :model="state" ref="formRef">
toRef 引用对象部分属性
参考资料 快速使用Vue3最新的15个常用API 参考资料 Vue3.0尝试
使用 toRef
可以将某个使用了 reactive 的响应对象中的某个属性单独拿出来,但这只是引用关系,所以修改这个引用的数据是会影响到原始数据的。
toRef 接收两个参数,第一个参数是哪个对象,第二个参数是对象的哪个属性
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs 将对象的属性变成 ref
toRefs()
函数可以将 reactive()
创建出来的响应式对象,转为普通对象,只不过这个对象上的每个属性都是以 ref()
工具创建出来的。
好处就是 <template>
里不用像对象一样先把属性 .
出来才能使用,使用场景如下
import { toRefs, reactive } from '@vue/composition-api'
setup() {
// 定义响应式数据对象
const state = reactive({
count: 0
})
// 定义页面上可用的事件处理函数
const increment = () => {
state.count++
}
// 在 setup 中返回一个对象供页面使用
// 这个对象中可以包含响应式的数据,也可以包含事件处理函数
return {
// 将 state 上的每个属性,都转化为 ref 形式的响应式数据(这个 ... 是 TS 的展开运算符)
...toRefs(state),
// 自增的事件处理函数
increment
}
}
在 template 中就直接可以使用 count 属性和相对应的 increment 方法了,如果没有使用 toRefs
直接返回 state 那么就得通过 state.xx
来访问数据
<template>
<div>
<span>当前的count值为:{{count}}</span>
<button @click="increment">add</button>
</div>
</template>
补充一下这个展开运算符
let list = [1, 2];
list = [...list, 3, 4];
console.log(list); // [1,2,3,4]
toRaw 直接操作原始值
toRaw 方法是用于直接获取 ref 或 reactive 对象的原始数据的引用,而不是 Proxy,当需要变更数据但是不想触发响应的时候就可以用它
<template>
<p>{{ state.name }}</p>
<p>{{ state.age }}</p>
<button @click="change">改变</button>
</template>
<script>
import {reactive} from 'vue'
export default {
setup() {
const obj = {
name: '前端印象',
age: 22
}
const state = reactive(obj)
function change() {
state.age = 90
console.log(obj); // 打印原始数据obj
console.log(state); // 打印 reactive对象
}
return {state, change}
}
}
</script>
如果这时更新原始数据 obj 的值,那 reactive 的值也会跟着改变,但是视图不更新。由此可见,当我们想修改数据,但不想让视图更新时,可以选择直接修改原始数据上的值,因此需要先获取到原始数据,我们可以使用 Vue3 提供的 toRaw 方法
<script>
import {reactive, toRaw} from 'vue'
export default {
setup() {
const obj = {
name: 'temp',
age: 22
}
const state = reactive(obj)
const raw = toRaw(state)
console.log(obj === raw) // true
}
}
</script>
注意: 当 toRaw 方法接收的参数是 ref 对象时,需要加上 .value
才能获取到原始数据对象
getCurrentInstance 使用全局对象
如何使用全局变量呢?在 Vue3 已经改变了 this
的指向,所以像 Element-plus 这种强依赖全局对象的工具如何取得绑定在全局对象上的变量呢?这时就可以使用 getCurrentInstance
取得当前实例
直接使用全局(注意加个 ? 安全链式调用)
import { defineComponent, reactive, getCurrentInstance } from "vue";
const message = getCurrentInstance()?.appContext.config.globalProperties.$message;
message({
showClose: true,
message: "错了哦,这是一条错误消息",
type: "error"
});
useStore 取得 Vuex 实例
在 Vue2 中使用 Vuex,都是通过 this.$store
来与获取到 Vuex 实例
在 Vue3 需要使用这个 useStore
方法
// store 文件夹下的 index.js
import Vuex from 'vuex'
const store = Vuex.createStore({
state: {
name: '前端印象',
age: 22
},
mutations: {
…… // 别忘了需要通过 mutations 才能变更 state 里面的值
},
……
})
// example.vue
// 从 vuex 中导入 useStore 方法
import {useStore} from 'vuex'
export default {
setup() {
// 获取 vuex 实例
const store = useStore()
console.log(store)
}
}
props 和 context
参考资料 组合式 API
props: {
title: String
},
setup(props, context) {
console.log(props.title)
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
context.emit('title-changed') // 就是之前的发射一个事件
}
props 插槽
当父子组件需要传递信息时就可以通过这个插槽
export default {
props: {
name: String
},
setup(props) {
console.log(props.name)
}
}
此 props 对象是响应式的——即在传入新的 props 时会对其进行更新,并且可以通过使用 watchEffect 或 watch 进行观测和响应:
export default {
props: {
name: String
},
setup(props) {
watchEffect(() => {
console.log(`name is: ` + props.name)
})
}
}
context 上下文
该对象暴露了以前在 this 上暴露的 property 的选择列表:
const MyComponent = {
setup(props, context) {
context.attrs
context.slots
context.emit
}
}
Vue3 使用 watch
<template>
<button @click="change">count is: {{ state.count }}</button>
</template>
<script>
import { reactive, watch } from 'vue'
export default {
setup () {
let state = reactive({count: 0})
let change = () => state.count++;
watch(state, () => {
console.log(state, '改变')
})
return { state, change }
}
}
</script>
在组件中监听 vuex 数据变化
//利用计算属性
computed: {
listData() {
return this.$store.state.listData;
}
},
//监听执行
watch: {
listData(val) {
写上你需要的东西
}
},